package invoke

import (
	"bytes"
	"strings"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/status"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"github.com/pkg/errors"

	"git.cloud.inspur.com/ichain/ichain-protos-go/common"
	pb "git.cloud.inspur.com/ichain/ichain-protos-go/inode"
)

type EndorsementHandler struct {
	next Handler
}

func (e *EndorsementHandler) Handle(requestContext *RequestContext, clientContext *ClientContext) {
	// 如果没有指定target则使用配置中的链的所有节点
	if len(requestContext.Opts.Targets) == 0 {
		requestContext.Opts.Targets, _ = clientContext.Discovery.GetInodes()
		if len(requestContext.Opts.Targets) == 0 {
			requestContext.Error = status.New(status.ClientStatus, status.NoInodesFound.ToInt32(), "targets were not provided", nil)
			return
		}
	}

	// 创建并发送背书提案
	transactionProposalResponses, proposal, err := createAndSendTransactionProposal(
		clientContext.Transactor,
		&requestContext.Request,
		requestContext.Opts.Targets)

	requestContext.Response.Proposal = proposal
	requestContext.Response.TransactionID = proposal.TxnID

	if err != nil {
		requestContext.Error = checkEndorserServerError(err)
		return
	}

	requestContext.Response.Responses = transactionProposalResponses
	if len(transactionProposalResponses) > 0 {
		requestContext.Response.Payload = transactionProposalResponses[0].ProposalResponse.GetResponse().Payload
		requestContext.Response.ContractStatus = transactionProposalResponses[0].ContractStatus
	}

	if e.next != nil {
		e.next.Handle(requestContext, clientContext)
	}
}

type EndorsementValidationHandler struct {
	next Handler
}

func (f *EndorsementValidationHandler) Handle(requestContext *RequestContext, clientContext *ClientContext) {

	err := f.validate(requestContext.Response.Responses)
	if err != nil {
		requestContext.Error = errors.WithMessage(err, "endorsement validation failed")
		return
	}

	if f.next != nil {
		f.next.Handle(requestContext, clientContext)
	}
}

func (f *EndorsementValidationHandler) validate(txProposalResponse []*ichain.TransactionProposalResponse) error {
	var a1 *pb.ProposalResponse
	for n, r := range txProposalResponse {
		response := r.ProposalResponse.GetResponse()
		if response.Status < int32(common.Status_SUCCESS) || response.Status >= int32(common.Status_BAD_REQUEST) {
			return status.NewFromProposalResponse(r.ProposalResponse, r.Endorser)
		}
		if n == 0 {
			a1 = r.ProposalResponse
			continue
		}

		if !bytes.Equal(a1.Payload, r.ProposalResponse.Payload) ||
			!bytes.Equal(a1.GetResponse().Payload, response.Payload) {
			return status.New(status.EndorserClientStatus, status.EndorsementMismatch.ToInt32(),
				"ProposalResponsePayloads do not match", nil)
		}
	}

	return nil
}

type CommitTxHandler struct {
	next Handler
}

func (c *CommitTxHandler) Handle(requestContext *RequestContext, clientContext *ClientContext) {
	txnID := requestContext.Response.TransactionID

	reg, statusNotifier, err := clientContext.EventService.RegisterTxStatusEvent(string(txnID)) // TODO: Change func to use TransactionID instead of string
	if err != nil {
		requestContext.Error = errors.Wrap(err, "error registering for TxStatus event")
		return
	}
	defer clientContext.EventService.Unregister(reg)

	_, err = createAndSendTransaction(clientContext.Transactor, requestContext.Response.Proposal, requestContext.Response.Responses)
	if err != nil {
		requestContext.Error = errors.Wrap(err, "CreateAndSendTransaction failed")
		return
	}

	select {
	case txStatus := <-statusNotifier:
		requestContext.Response.TxValidationCode = txStatus.TxValidationCode

		if txStatus.TxValidationCode != pb.TxValidationCode_VALID {
			requestContext.Error = status.New(status.EventServerStatus, int32(txStatus.TxValidationCode),
				"received invalid transaction", nil)
			return
		}
	case <-requestContext.Ctx.Done():
		requestContext.Error = status.New(status.ClientStatus, status.Timeout.ToInt32(),
			"Execute didn't receive block event", nil)
		return
	}

	if c.next != nil {
		c.next.Handle(requestContext, clientContext)
	}
}

// NewQueryHandler 调用合约的query方法
func NewQueryHandler(next ...Handler) Handler {
	return NewEndorsementHandler(
		NewEndorsementValidationHandler(
			NewSignatureValidationHandler(next...),
		),
	)
}

func NewExecuteHandler(next ...Handler) Handler {
	return NewEndorsementHandler(
		NewEndorsementValidationHandler(
			NewSignatureValidationHandler(
				NewCommitHandler(next...)),
		))
}

func NewEndorsementHandler(next ...Handler) *EndorsementHandler {
	return &EndorsementHandler{next: getNext(next)}
}

func NewEndorsementValidationHandler(next ...Handler) *EndorsementValidationHandler {
	return &EndorsementValidationHandler{next: getNext(next)}
}

func NewCommitHandler(next ...Handler) *CommitTxHandler {
	return &CommitTxHandler{next: getNext(next)}
}

func getNext(next []Handler) Handler {
	if len(next) > 0 {
		return next[0]
	}
	return nil
}

func createAndSendTransaction(sender ichain.Sender, proposal *ichain.TransactionProposal, resps []*ichain.TransactionProposalResponse) (*ichain.TransactionResponse, error) {

	txnRequest := ichain.TransactionRequest{
		Proposal:          proposal,
		ProposalResponses: resps,
	}

	tx, err := sender.NewTx(txnRequest)
	if err != nil {
		return nil, errors.WithMessage(err, "CreateTransaction failed")
	}

	transactionResponse, err := sender.SendTx(tx)
	if err != nil {
		return nil, errors.WithMessage(err, "SendTransaction failed")

	}

	return transactionResponse, nil
}

func createAndSendTransactionProposal(transactor ichain.Transactor, chrequest *Request, targets []ichain.Inode) ([]*ichain.TransactionProposalResponse, *ichain.TransactionProposal, error) {
	request := ichain.ContractInvokeRequest{
		ContractID:   chrequest.ContractID,
		Fcn:          chrequest.Fcn,
		Args:         chrequest.Args,
		TransientMap: chrequest.TransientMap,
		IsInit:       chrequest.IsInit,
	}

	proposal, err := transactor.CreateTxProposal(request)
	if err != nil {
		return nil, nil, errors.WithMessage(err, "creating transaction header failed")
	}

	transactionProposalResponses, err := transactor.SendTxProposal(proposal, targets)

	return transactionProposalResponses, proposal, err
}

func checkEndorserServerError(err error) error {
	if strings.Contains(err.Error(), "failed to distribute private collection") {
		return status.New(status.EndorserServerStatus, status.PvtDataDisseminationFailed.ToInt32(), err.Error(), nil)
	}

	return err
}
